/*******************************************************************************
 * Copyright (c) 2011 TXT e-solutions SpA
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * This work was performed within the IoT_at_Work Project
 * and partially funded by the European Commission's
 * 7th Framework Programme under the research area ICT-2009.1.3
 * Internet of Things and enterprise environments.
 *
 *
 * Authors:
 *     Cristoforo Seccia (TXT e-solutions SpA)
 *
 * Contributors:
 *      Domenico Rotondi (TXT e-solutions SpA)
 *******************************************************************************/
package it.txt.access.capability.demo.soap.server.service.impl;

import it.txt.access.capability.commons.schema.util.CapabilitySchemasPrinter;
import it.txt.access.capability.commons.schema.validation.CapabilitySchemaValidationHandler;
import it.txt.access.capability.commons.schema.validation.CapabilitySchemaValidationHandlerException;
import it.txt.access.capability.commons.utils.CalendarUtils;
import it.txt.access.capability.commons.utils.ClientServerUtils;
import it.txt.access.capability.factory.CapabilitySchemaFactoryException;
import it.txt.access.capability.factory.CapabilitySchemaFactory;
import it.txt.access.capability.factory.values.X509SubjectInfo;
import it.txt.access.capability.demo.schema.CapabilityRequestType;
import it.txt.access.capability.demo.schema.CapabilityResponseType;
import it.txt.access.capability.demo.soap.server.model.ServiceRequestInfoModel;
import it.txt.access.capability.schema.AccessRightType;
import it.txt.access.capability.schema.AccessRightsCapabilityType;
import it.txt.access.capability.demo.soap.server.model.ServerKeystoreDataModel;
import it.txt.access.capability.demo.soap.server.model.ServerResponseInfoModel;
import it.txt.access.capability.demo.schema.factory.CapabilityDemoSchemaFactoryException;
import it.txt.access.capability.demo.schema.factory.CapabilityDemoSchemaFactory;
import it.txt.access.capability.demo.soap.server.service.CapabilityServiceException;
import it.txt.access.capability.verifier.CapabilityVerifier;
import it.txt.access.capability.verifier.CapabilityVerifierException;
import it.txt.access.capability.verifier.VerifiedCapability;
import it.txt.access.capability.verifier.security.KeystoreHelper;
import it.txt.access.capability.verifier.security.KeystoreSecurityException;
import it.txt.access.capability.verifier.security.KeystoreSignature;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.core.io.ResourceLoader;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import org.w3c.dom.Element;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.xml.datatype.XMLGregorianCalendar;
import javax.xml.transform.Source;
import javax.xml.transform.dom.DOMSource;
import org.w3c.dom.Document;

/**
 *
 * @author Cristoforo Seccia (TXT e-solutions SpA)
 */
public class CapabilityServiceHelper implements ResourceLoaderAware {

    private static final Logger LOGGER = Logger.getLogger(CapabilityServiceHelper.class.getName());
    //Used by the server into the xml response if the specified resource id is not the same as
    //the one specified in the capability.
    public static final String RESPONSE_INVALID_RESOURCE_ID = "The Resoure specified in the "
            + "request do not match the Capability Resource ID.";
    //Used by the server into the xml response if the specified operation is not contained in
    //the action rights of the capability.
    public static final String RESPONSE_INVALID_CAPABILITY_OPERATION = "The Operation specified in the "
            + "request is not specified in the Capability.";
    //Used by the server into the xml response if the operation is contained within the capability but
    //is not contained in the well known crud operation list.
    public static final String RESPONSE_INVALID_CRUD_OPERATION = "Can not perform the operation "
            + "specified in the request becouse it is not a standard CRUD operations.";
    public static final String RESPONSE_OPERATION_RESOURCE_NOT_EXIST = "The operation "
            + "can not be performed becouse the file do not exist.";
    //Used by the server after the RESPONSE_CAPABILITY_VALID if the operation Create can not be performed.
    public static final String RESPONSE_OPERATION_CREATE_ALREADY_EXIST = "The Create operation "
            + "can not be performed becouse the file already exist.";
    //Used by the server when creating a new file as specified by the capability request.
    public static final String OPERATION_CREATE_FILE_CONTENT = "This file was created by the "
            + "TXT Capability-Based Authorization System in order to perfom the Create operation.";
    //Used by the server after the RESPONSE_CAPABILITY_VALID if the operation Delete can not be performed.
    public static final String RESPONSE_OPERATION_DELETE_NOT_PERFORMED = "The Delete operation "
            + "can not be performed becouse the resource is not a file or is not an empty directory.";
    public static final String RESPONSE_OPERATION_CREATE_NOT_PERFORMED = "The Create operation "
            + "can not be performed.";
    private static KeystoreSignature.CertificateKeyValues CERTIFICATE_KEY_VALUES;
    private static ServerKeystoreDataModel KEYSTORE_DATA;
    private static KeystoreSignature KEYSTORE_SIGN;
    private static ResourceLoader RESOURCE_LOADER;

    public ServiceRequestInfoModel checkRequestCapability(CapabilityRequestType capabilityRequest) throws CapabilityServiceException {
        try {
            Document document = CapabilityDemoSchemaFactory.getCapabilityRequestToDocument(capabilityRequest);
            return checkRequestCapability(document.getDocumentElement());
        } catch (CapabilityDemoSchemaFactoryException ex) {
            throw new CapabilityServiceException("Error while recovering Request document.", ex);
        }
    }

    public ServiceRequestInfoModel checkRequestCapability(Element capabilityRequest) throws CapabilityServiceException {

        CapabilitySchemasPrinter.prettyLogDocumentElement(capabilityRequest, "Receives request");
        ServiceRequestInfoModel dataModel = new ServiceRequestInfoModel();
        X509SubjectInfo requestSubjectInfo = new X509SubjectInfo();
        CapabilityRequestType crt;
        // Check the Capability Request has not been tampered!
        try {
            Source source = new DOMSource(capabilityRequest);
            crt = CapabilityDemoSchemaFactory.getCapabilityRequestFromSource(source);
            KeystoreSignature signature = new KeystoreSignature();
            if (!signature.verifyElementSignature(capabilityRequest, requestSubjectInfo)) {
                throw new CapabilityServiceException("The Request Document Sign is Invalid");
            }
        }
        catch (CapabilityDemoSchemaFactoryException ex) {
            throw new CapabilityServiceException("Error while recovering Request document.", ex);
        }
        catch (KeystoreSecurityException ex) {
            throw new CapabilityServiceException("The Request Document Sign is Invalid.", ex);
        }
        //Verify the capability through the CapabilityVerifier
        AccessRightsCapabilityType capability = crt.getRequestAccessRightsCapability().getAccessRightsCapability();
        try {
            ArrayList<VerifiedCapability> vcs = new ArrayList<VerifiedCapability>();
            if (!CapabilityVerifier.accessRightsCapabilityValidityCheck(capability,vcs)) {
                String message = "Access Rights Capability is well formed but "
                        + "did not pass the validity check.";
                throw new CapabilityServiceException(message);
            }
        } catch (CapabilityVerifierException ex) {
            throw new CapabilityServiceException(ex.getMessage(), ex);
        } catch (CapabilitySchemaValidationHandlerException ex) {
            String errors = CapabilitySchemaValidationHandler.dumpValidationEvents(ex.getValidationEvents());
            throw new CapabilityServiceException(errors, ex);
        }
        //Check that the signer of the request is the subject of the capability
        String capabilitySubject = capability.getSubject().getSubjectID().getValue();
        capabilitySubject = capabilitySubject.trim().replace(" ", "");
        capabilitySubject = capabilitySubject.toUpperCase();
        String requestSubject = requestSubjectInfo.getX509SubjectName();
        requestSubject = requestSubject.trim().replace(" ", "");
        requestSubject = requestSubject.toUpperCase();
        if (!requestSubject.contains(capabilitySubject)) {
            String message = "The Subject used to sign the request is not "
                    + "equal to the subject specified in the Capability.";
            throw new CapabilityServiceException(message);
        }
        //Check thet the request issue time is included in the capability
        //validation time range.
        XMLGregorianCalendar requestIssueTime = crt.getRequestIstant();
        XMLGregorianCalendar capabilityNotBeforeTime = capability.getValidityCondition().getNotBefore();
        XMLGregorianCalendar capabilityNotOrOnAfterTime = capability.getValidityCondition().getNotOnOrAfter();
        int checkRequestIssueTimeToNotBeforeTime = CalendarUtils.comapareDataToAnotherDate(requestIssueTime, capabilityNotBeforeTime);
        int checkRequestIssueTimeToNotOrOnAfterTime = CalendarUtils.comapareDataToAnotherDate(requestIssueTime, capabilityNotOrOnAfterTime);
        if (checkRequestIssueTimeToNotBeforeTime != CalendarUtils.AFTER_THAN) {
            String message = "The request issue time is not 'after' "
                    + "the Capability 'not before' time.";
            throw new CapabilityServiceException(message);
        }
        if (checkRequestIssueTimeToNotOrOnAfterTime != CalendarUtils.BEFORE_THAN) {
            String message = "The request issue time is not 'before' "
                    + "the Capability 'not or on after' time.";
            throw new CapabilityServiceException(message);
        }
        //Everything is fine, now get the model
        dataModel.setRequestCapability(capability);
        dataModel.setRequestID(crt.getCapabilityRequestID());
        dataModel.setResourceID(crt.getResourceID());
        dataModel.setOperation(crt.getOperation());
        dataModel.setCapabilityID(capability.getAccessRightsCapabilityID());
        dataModel.setSigner(requestSubjectInfo.getX509SubjectName());
        dataModel.setSignerKeyIssuer(requestSubjectInfo.getX509IssuerName());
        dataModel.setSignerKeyID(requestSubjectInfo.getX509SerialNumber().toString(16));
        return dataModel;
    }

    public void performRequestOperation(ServiceRequestInfoModel infoModel) throws CapabilityServiceException {
        File requestFile;
        //Recover request operation.
        String operation = infoModel.getOperation();
        //Recover request resource.
        String resourceID = infoModel.getResourceID();
        //Recover the file specified by the resource.
        //Before perfor the operation, check if the request is valid
        checkRequestOperation(infoModel);
        try {
            requestFile = new File(new URI(resourceID));
        }
        catch (Exception ex) {
            //The Resource is not an URi;
            LOGGER.log(Level.INFO, ex.getMessage(), ex);
            requestFile = new File(resourceID);
        }
        LOGGER.log(Level.INFO, "The Resource ID Path: {0}", requestFile.getAbsolutePath());
        //Check the operation type
        if (operation.equalsIgnoreCase(ClientServerUtils.OPERATION_UPDATE)) {
            if (!requestFile.exists()) {
                //can not perform update operation.
                throw new CapabilityServiceException(RESPONSE_OPERATION_RESOURCE_NOT_EXIST);
            }
        }
        else if (operation.equalsIgnoreCase(ClientServerUtils.OPERATION_READ)) {
            if (!requestFile.exists()) {
                //can not perform read operation.
                throw new CapabilityServiceException(RESPONSE_OPERATION_RESOURCE_NOT_EXIST);
            }
        }
        else if (operation.equalsIgnoreCase(ClientServerUtils.OPERATION_DELETE)) {
            if (!requestFile.exists()) {
                //can not perform delete operation.
                throw new CapabilityServiceException(RESPONSE_OPERATION_RESOURCE_NOT_EXIST);
            } else {
                //can not perform delete operation over directory.
                if (requestFile.isDirectory()) {
                    throw new CapabilityServiceException(RESPONSE_OPERATION_DELETE_NOT_PERFORMED);
                } else if (requestFile.isFile()) {
                    if (!requestFile.delete()) {
                        //can not perform delete operation.
                        throw new CapabilityServiceException(RESPONSE_OPERATION_DELETE_NOT_PERFORMED);
                    }
                } else {
                    //The resourceID is not a file.
                    throw new CapabilityServiceException(RESPONSE_OPERATION_DELETE_NOT_PERFORMED);
                }
            }
        } else if (operation.equalsIgnoreCase(ClientServerUtils.OPERATION_CREATE)) {
            if (requestFile.exists()) {
                //can not perform create operation.
                throw new CapabilityServiceException(RESPONSE_OPERATION_CREATE_ALREADY_EXIST);
            } else {
                try {
                    //write something to the created file
                    FileWriter fileWriter = new FileWriter(requestFile.toURI().getPath());
                    fileWriter.append(OPERATION_CREATE_FILE_CONTENT);
                    fileWriter.close();
                } catch (IOException ex) {
                    String response = "";
                    response += ClientServerUtils.RESPONSE_INTERNAL_ERROR;
                    response += " ";
                    response += RESPONSE_OPERATION_CREATE_NOT_PERFORMED;
                    throw new CapabilityServiceException(response, ex);
                }
            }
        } else {
            //due the checkRequestValidity this case should not been performed.
            throw new CapabilityServiceException(ClientServerUtils.RESPONSE_REQUEST_OPERATION_NOT_PERFORMED);
        }
    }

    private void checkRequestOperation(ServiceRequestInfoModel infoModel) throws CapabilityServiceException {

        AccessRightsCapabilityType capability = infoModel.getRequestCapability();
        String capabilityResource = capability.getResourceID();
        String requestResource = infoModel.getResourceID();
        if (!capabilityResource.equals(requestResource)) {
            throw new CapabilityServiceException(RESPONSE_INVALID_RESOURCE_ID);
        }
        List<AccessRightType> accessRights = capability.getAccessRights().getAccessRight();
        String requestOperation = infoModel.getOperation();
        boolean requestOperationFound = false;
        for (AccessRightType accessRightType : accessRights) {
            String action = accessRightType.getPermittedAction().getValue();
            if (action.equalsIgnoreCase(requestOperation)) {
                requestOperationFound = true;
                break;
            }
        }
        if (!requestOperationFound) {
            throw new CapabilityServiceException(RESPONSE_INVALID_CAPABILITY_OPERATION);
        }
        boolean crudOperationFound = false;
        for (String crudOperation : ClientServerUtils.CRUD_LIST) {
            if (crudOperation.equalsIgnoreCase(requestOperation)) {
                crudOperationFound = true;
                break;
            }
        }
        if (!crudOperationFound) {
            throw new CapabilityServiceException(RESPONSE_INVALID_CRUD_OPERATION);
        }
    }

    public Element createSignedResponse(ServerResponseInfoModel responseInfoModel) throws CapabilityServiceException {
        try {
            String responseStatus = responseInfoModel.getResponseStatus();
            String requestID = responseInfoModel.getRequestID();
            CapabilityResponseType serverCapabilityResponse = CapabilityDemoSchemaFactory.createCapabilityResponse(responseStatus, requestID);
            Document document = CapabilityDemoSchemaFactory.getFragmentCapabilityResponseToDocument(serverCapabilityResponse);
            Element serverResponse = document.getDocumentElement();
            KEYSTORE_SIGN.signElementObject(serverResponse, CERTIFICATE_KEY_VALUES, "CapabilityResponseID");
            return serverResponse;
        } catch (CapabilityDemoSchemaFactoryException ex) {
            throw new CapabilityServiceException(ex.getMessage(), ex);
        } catch (KeystoreSecurityException ex) {
            throw new CapabilityServiceException(ex.getMessage(), ex);
        }
    }

    @Override
    public void setResourceLoader(ResourceLoader resourceLoader) {
        try {
            KeyStore keyStore = null;
            RESOURCE_LOADER = resourceLoader;
            String schemas = RESOURCE_LOADER.getResource("Capability_Schemas").getFile().getPath();
            CapabilitySchemaFactory.initCapabilityCreator(schemas);
            LOGGER.log(Level.INFO, "Capability Creator Initialized successfully.");
            File keystore = RESOURCE_LOADER.getResource("keystore/capability_keystore.jks").getFile();
            LOGGER.log(Level.INFO, "Set Resource Loader: {0}", keystore.getPath());
            KEYSTORE_DATA.setKeystorePath(keystore.getPath());
            KEYSTORE_DATA.setServerID("demoserver01@acme.com");
            KEYSTORE_DATA.setKeystorePsw("sysmgr".toCharArray());
            KEYSTORE_DATA.setPrivateKeyPsw("sysmgr".toCharArray());
            java.security.cert.X509Certificate x509Certificate = null;
            try {
                //Recover the Keystore.
                keyStore = KeystoreHelper.getKeyStore(KEYSTORE_DATA.getKeystorePath(), KEYSTORE_DATA.getKeystorePsw());
                //Recover the certificate from the Keystore
                x509Certificate = KeystoreHelper.getX509CertificateByIssuer(keyStore, KEYSTORE_DATA.getServerID());
                //Check if we have found a certificate with the provided issuer.
                if (x509Certificate != null) {
                    try {
                        //Recover the alias of the certificate
                        String SERVER_ALIAS = KeystoreHelper.getAlias(keyStore, x509Certificate);
                        //Recover the private key of the.
                        KEYSTORE_SIGN = new KeystoreSignature();
                        //Recover the server certificate key values
                        CERTIFICATE_KEY_VALUES = KEYSTORE_SIGN.getCertificateKeyValues(
                                KEYSTORE_DATA.getKeystorePath(),
                                KEYSTORE_DATA.getPrivateKeyPsw(),
                                SERVER_ALIAS,
                                KEYSTORE_DATA.getPrivateKeyPsw());
                    } catch (KeyStoreException ex) {
                        LOGGER.log(Level.SEVERE,
                                "Cannot recover the alias of the certificate: {0}",
                                ex.getMessage());
                    } catch (KeystoreSecurityException ex) {
                        LOGGER.log(Level.SEVERE,
                                "Error while recovering certificate private key: {0}",
                                ex.getMessage());
                    }
                } else {
                    LOGGER.log(Level.SEVERE, "Cannot find a certificate by using.");
                }
            } catch (KeystoreSecurityException ex) {
                LOGGER.log(Level.SEVERE, ex.getMessage());
            } catch (GeneralSecurityException ex) {
                LOGGER.log(Level.SEVERE,
                        "Error while searching for certificate into keystore: {0}",
                        ex.getMessage());
            }
        }
        catch (CapabilitySchemaFactoryException ex) {
            LOGGER.log(Level.SEVERE, ex.getMessage(), ex);
        }
        catch (IOException ex) {
            LOGGER.log(Level.SEVERE, ex.getMessage(), ex);
        }
    }
}
